Khai báo và sử dụng Class trong C++

    Các lớp C++ có các thành viên riêng của chúng. Các thành viên này bao gồm các biến (bao gồm các cấu trúc và lớp khác), các hàm (các định danh cụ thể hoặc các toán tử nạp chồng) được gọi là các phương thức, các hàm tạo và các hàm hủy. Các thành viên được tuyên bố là có thể truy cập công khai hoặc riêng tư bằng cách sử dụng công cụ xác định quyền truy cập public:private: tương ứng. Bất kỳ thành viên nào gặp sau một mã xác định sẽ có quyền truy cập liên quan cho đến khi gặp một bộ chỉ định khác. Ngoài ra còn có sự kế thừa giữa các lớp có thể sử dụng protected:

    Lớp toàn cục và lớp cục bộ

    Một lớp được định nghĩa bên ngoài tất cả các phương thức là một lớp toàn cục vì các đối tượng của nó có thể được tạo từ bất kỳ đâu trong chương trình. Nếu nó được định nghĩa trong một thân hàm thì đó là một lớp cục bộ vì các đối tượng của lớp như vậy là cục bộ đối với phạm vi hàm.

    Khai báo cơ bản và các biến thành viên

    Các lớp được khai báo với từ khóa class hoặc struct. Tuyên bố của các thành viên được đặt trong tuyên bố này.

    1struct Person {23  string name;4  int age;5};
    1class Person {2 public:3  string name;4  int age;5};

    Các định nghĩa trên là tương đương về mặt chức năng. Một trong hai mã sẽ xác định các đối tượng kiểu Person là có hai thành viên dữ liệu công khai, nameage. Dấu chấm phẩy sau dấu ngoặc nhọn là bắt buộc.Sau một trong các khai báo này (nhưng không phải cả hai), Person có thể được sử dụng như sau để tạo các biến mới được xác định của kiểu dữ liệu Person:

    #include <iostream>#include <string>struct Person {  std::string name;  int age;};int main() {  Person a;  Person b;  a.name = "Calvin";  b.name = "Hobbes";  a.age = 30;  b.age = 20;  std::cout << a.name << ": " << a.age << std::endl;  std::cout << b.name << ": " << b.age << std::endl;}

    Khi chạy chương trình trên sẽ in ra màn hình:

    Calvin: 30Hobbes: 20

    Chức năng thành viên

    Một tính năng quan trọng của lớp và cấu trúc C ++ là các hàm thành viên. Mỗi kiểu dữ liệu có thể có các hàm tích hợp riêng của nó (được gọi là các phương thức) có quyền truy cập vào tất cả các thành viên (public và private) của kiểu dữ liệu. Trong phần thân của các hàm thành viên không tĩnh này, từ khóa this có thể được sử dụng để tham chiếu đến đối tượng mà hàm được gọi. Điều này thường được thực hiện bằng cách chuyển địa chỉ của đối tượng như một đối số ngầm định đầu tiên cho hàm. Lấy kiểu Person ở trên làm ví dụ một lần nữa:

    #include <iostream>class Person { public:  void Print() const; private:  std::string name_;  int age_ = 5;};void Person::Print() const {  std::cout << name_ << ":" << age_ << std::endl;  // "name_" và "age_" là các biến thành viên. Từ "keyword" là  // biểu thức có giá trị là địa chỉ của đối tượng mà thành viên  // đã được gọi. Kiểu của nó là "const Person *", vì hàm được khai báo hằng số

    Trong ví dụ trên, hàm Print được khai báo trong phần thân của lớp và được định nghĩa bằng cách định nghĩa nó với tên của lớp theo sau là ::. Cả name_age_ đều là private (mặc định cho lớp) và Print được khai báo là public, cần thiết nếu nó được sử dụng từ bên ngoài lớp.Với chức năng thành viên Print, việc in ấn có thể được đơn giản hóa thành:

    a.Print();b.Print();

    Trong đó ab ở trên được gọi là người gửi và mỗi người trong số họ sẽ tham chiếu đến các biến thành viên của riêng chúng khi hàm Print() được thực thi.

    Thực tế phổ biến là tách khai báo lớp hoặc cấu trúc (được gọi là giao diện của nó) và định nghĩa (được gọi là hiện thực của nó) thành các đơn vị riêng biệt. Giao diện, được người dùng cần, được giữ trong tiêu đề và việc triển khai được giữ riêng ở dạng nguồn hoặc dạng biên dịch.

    Sự thừa kế

    Việc bố trí các lớp không phải POD trong bộ nhớ không được tiêu chuẩn C ++ chỉ định. Ví dụ, nhiều trình biên dịch C ++ phổ biến thực hiện kế thừa đơn bằng cách nối các trường lớp cha với các trường lớp con, nhưng tiêu chuẩn này không yêu cầu. Lựa chọn bố cục này làm cho việc tham chiếu đến một lớp dẫn xuất thông qua một con trỏ đến kiểu lớp cha là một hoạt động tầm thường.

    Ví dụ, hãy xem xét

    struct P {  int x;};
    struct C : P {  int y;};

    Một ví dụ của P với P* p trỏ tới nó có thể trông như thế này trong bộ nhớ:

    +----+|P::x|+----+↑p

    Một ví dụ của C với P* p trỏ đến nó có thể trông như thế này:

    +----+----+|P::x|C::y|+----+----+↑p

    Do đó, bất kỳ mã nào thao tác với các trường của một đối tượng P đều có thể thao tác các trường P bên trong đối tượng C mà không cần phải xem xét bất kỳ điều gì về định nghĩa các trường của C. Một chương trình C++ được viết đúng cách không nên đưa ra bất kỳ giả định nào về bố cục của các trường kế thừa, trong mọi trường hợp. Sử dụng toán tử chuyển đổi kiểu static_cast hoặc dynamic_cast sẽ đảm bảo rằng các con trỏ được chuyển đổi đúng cách từ kiểu này sang kiểu khác.

    Đa kế thừa không phải là đơn giản. Nếu một lớp D kế thừa PC, thì các trường của cả hai trường cha mẹ cần được lưu trữ theo một số thứ tự, nhưng (tối đa) chỉ một trong các lớp cha có thể được đặt ở phía trước của lớp dẫn xuất. Bất cứ khi nào trình biên dịch cần chuyển đổi con trỏ từ kiểu D sang P hoặc C, trình biên dịch sẽ cung cấp chuyển đổi tự động từ địa chỉ của lớp dẫn xuất sang địa chỉ của các trường lớp cơ sở (thông thường, đây là một phép tính bù đơn giản).

    Để biết thêm về đa kế thừa, hãy xem kế thừa ảo.

    Nạp chồng toán tử

    Trong C++, các toán tử, chẳng hạn như + - * /, có thể được nạp chồng để phù hợp với nhu cầu của người lập trình. Các toán tử này được gọi là toán tử có thể nạp chồng.

    Theo quy ước, các toán tử được nạp chồng sẽ hoạt động gần giống như chúng hoạt động trong các kiểu dữ liệu dựng sẵn (int, float, v.v.), nhưng điều này là không bắt buộc. Người ta có thể khai báo một cấu trúc gọi là Integer, trong đó biến thực sự lưu trữ một số nguyên, nhưng bằng cách gọi Integer * Integer, tổng, thay vì tích, của các số nguyên có thể được trả về:

    struct Integer {  Integer(int j = 0): i(j) {}  Integer operator*(const Integer& k) const {    return Integer(i + k.i);  }  int i;};

    Đoạn mã trên đã sử dụng một hàm tạo để "tạo" giá trị trả về. Để trình bày rõ ràng hơn (mặc dù điều này có thể làm giảm hiệu quả của chương trình nếu trình biên dịch không thể tối ưu hóa câu lệnh thành câu lệnh tương đương ở trên), đoạn mã trên có thể được viết lại thành:

    Integer operator*(const Integer& k) const {  Integer m;  m.i = i + k.i;  return m;}

    Lập trình viên cũng có thể đặt một nguyên mẫu của toán tử trong khai báo struct và xác định chức năng của toán tử trong phạm vi toàn cục:

    struct Integer {  Integer(int j = 0): i(j) {}  Integer operator*(const Integer& k) const;  int i;}; Integer Integer::operator*(const Integer& k) const {  return Integer(i * k.i);}

    i ở trên đại diện cho biến thành viên của chính người gửi, trong khi k.i đại diện cho biến thành viên từ biến đối số k.

    Từ khóa const xuất hiện hai lần trong đoạn mã trên. Lần xuất hiện đầu tiên, đối số const integer& k, chỉ ra rằng biến đối số sẽ không bị thay đổi bởi hàm. Tỷ lệ thứ hai ở cuối khai báo hứa với trình biên dịch rằng người gửi sẽ không bị thay đổi bởi hàm chạy.

    Trong const integer& k, dấu và (&) có nghĩa là "chuyển qua tham chiếu". Khi hàm được gọi, một con trỏ tới biến sẽ được chuyển tới hàm, thay vì giá trị của biến.

    Các thuộc tính nạp chồng tương tự ở trên cũng áp dụng cho các lớp.

    Lưu ý rằng không thể thay đổi độ hiếm, tính liên kết và thứ tự ưu tiên của các toán tử.

    Nạp chồng toán tử nhị phân

    Toán tử nhị phân (toán tử có hai đối số) được nạp chồng bằng cách khai báo một hàm với một toán tử "định danh" (một cái gì đó) gọi một đối số duy nhất. Biến ở bên trái của toán tử là người gửi trong khi ở bên phải là đối số.

    Integer i = 1; /* chúng ta có thể khởi tạo một biến cấu trúc theo cách    này nếu gọi một hàm tạo chỉ có đối số được chỉ định. */Integer j = 3;/* tên biến độc lập với tên của   các biến thành viên của cấu trúc. */Integer k = i * j;std::cout << k.i << std::endl;

    Sẽ in ra màn hình là

    3

    Sau đây là danh sách các toán tử có thể nạp chồng nhị phân:

    Toán tửCông dụng chung
    + - * / %Tính toán số học
    ^ & | << >>Tính toán thao tác bit
    < > == != <= >=So sánh logic
    &&Kết hợp logic
    ||Sự phân chia logic
    += -= *= /= %=^= &= |= <<= >>=Phân công tổng hợp
    ,(không sử dụng chung)

    Toán tử '=' (gán) giữa hai biến có cùng kiểu cấu trúc được nạp chồng theo mặc định để sao chép toàn bộ nội dung của biến từ biến này sang biến khác. Nó có thể được ghi đè bằng thứ khác, nếu cần.

    Các toán tử phải được nạp chồng từng cái một, nói cách khác, không có quá tải nào được liên kết với nhau. Ví dụ, <không nhất thiết phải ngược lại với>.

    Toán tử có thể nạp chồng một lần

    Trong khi một số toán tử, như được chỉ định ở trên, nhận hai điều khoản, người gửi ở bên trái và đối số ở bên phải, một số toán tử chỉ có một đối số - người gửi và chúng được cho là "một ngôi". Ví dụ là dấu phủ định (khi không có gì được đặt ở bên trái của nó) và "logic NOT" (dấu chấm than,!).

    Người gửi của toán tử một ngôi có thể ở bên trái hoặc bên phải của nhà khai thác. Sau đây là danh sách các toán tử có thể nạp chồng một lần:

    Toán tửCông dụng chungVị trí sử dụng
    + -Cộng / trừphải
    * &Con trỏphải
    ! ~Logical / bitwise NOTphải
    ++ --Tăng / giảm trướcphải
    ++ --Tăng / giảm sautrái

    Cú pháp nạp chồng toán tử một ngôi, trong đó người gửi ở bên phải, như sau:

    Kiểu_trả_về toán_tử@ ()

    Khi người gửi ở bên trái, khai báo là:

    kiểu_trả_về toán_tử@ (int)

    @ trên là viết tắt của toán tử được quá tải. Thay thế return_type bằng kiểu dữ liệu của giá trị trả về (int, bool, cấu trúc, v.v.)

    Tham số int về cơ bản không có nghĩa gì ngoài một quy ước để cho thấy rằng người gửi ở bên trái của toán tử.

    Đối số const có thể được thêm vào cuối khai báo nếu có.

    Mảng có thể nạp chồng

    Dấu ngoặc vuông [] và dấu ngoặc tròn () có thể được nạp chồng trong cấu trúc C++. Dấu ngoặc vuông phải chứa chính xác một đối số, trong khi dấu ngoặc tròn có thể chứa bất kỳ số lượng đối số cụ thể nào hoặc không có đối số nào.

    Khai báo mảng có thể nạp chồng

    Kiểu_trả_về toán_tử[] (tham số)

    Nội dung bên trong dấu ngoặc được chỉ định trong tham số.

    Dấu ngoặc tròn được nạp chồng theo cách tương tự.

    Kiểu_trả_về toán_tử() (tham_số_1,tham_số_2,...)

    Nội dung của dấu ngoặc trong lệnh gọi nhà điều hành được chỉ định trong dấu ngoặc thứ hai.

    Ngoài các toán tử được chỉ định ở trên, toán tử mũi tên (->), mũi tên có dấu sao (-> *), từ khóa new và từ khóa delete cũng có thể được nạp chồng. Các toán tử liên quan đến bộ nhớ hoặc con trỏ này phải xử lý các chức năng cấp phát bộ nhớ sau khi nạp chồng. Giống như toán tử gán (=), chúng cũng được nạp chồng theo mặc định nếu không có khai báo cụ thể.

    Hàm tạo

    Đôi khi các lập trình viên có thể muốn các biến của họ nhận một giá trị mặc định hoặc một giá trị cụ thể khi khai báo. Điều này có thể được thực hiện bằng cách khai báo các hàm tạo.

    Person::Person(string name, int age) {  name_ = name;  age_ = age;}

    Các biến thành viên có thể được khởi tạo trong danh sách trình khởi tạo, với việc sử dụng dấu hai chấm, như trong ví dụ dưới đây. Điều này khác với ở trên ở chỗ nó khởi tạo (sử dụng hàm tạo), thay vì sử dụng toán tử gán. Điều này hiệu quả hơn đối với các loại lớp, vì nó chỉ cần được xây dựng trực tiếp; trong khi với phép gán, chúng phải được khởi tạo đầu tiên bằng cách sử dụng hàm tạo mặc định, sau đó được gán một giá trị khác. Ngoài ra, một số kiểu (như tham chiếu và kiểu const) không thể được gán cho và do đó phải được khởi tạo trong danh sách trình khởi tạo.

    Person(std::string name, int age) : name_(name), age_(age) {}

    Lưu ý rằng không thể bỏ qua dấu ngoặc nhọn, ngay cả khi trống.Giá trị mặc định có thể được cấp cho các đối số cuối cùng để giúp khởi tạo các giá trị mặc định.

    Person(std::string name = "", int age = 0) : name_(name), age_(age) {}

    Khi không có đối số nào được cung cấp cho hàm tạo trong ví dụ trên, nó tương đương với việc gọi hàm tạo sau không có đối số (một hàm tạo mặc định):

    Person() : name_(""), age_(0) {}

    Khai báo của một phương thức khởi tạo trông giống như một hàm có cùng tên với kiểu dữ liệu. Trên thực tế, một lời gọi đến một phương thức khởi tạo có thể ở dạng một lời gọi hàm. Trong trường hợp đó, một biến kiểu Person được khởi tạo có thể được coi là giá trị trả về:

    int main() {  Person r = Person("Wales", 40);  r.Print();}

    Một cú pháp thay thế thực hiện tương tự như ví dụ trên là

    int main() {  Person r("Wales", 40);  r.Print();}

    Các hành động chương trình cụ thể, có thể có hoặc không liên quan đến biến, có thể được thêm vào như một phần của hàm tạo.

    Person() {  std::cout << "Hello!" << std::endl;}

    Với hàm tạo ở trên, "Xin chào!" sẽ được in khi phương thức khởi tạo Person mặc định được gọi.

    Hàm tạo mặc định

    Các hàm tạo mặc định được gọi khi các hàm tạo không được định nghĩa cho các lớp.

    struct A {  int b;};// Đối tượng được tạo bằng cách sử dụng dấu ngoặc đơn.A* a = new A();  // Gọi hàm tạo mặc định và b sẽ được khởi tạo bằng '0'.// Đối tượng được tạo không sử dụng dấu ngoặc đơn.A* a = new A;  // Phân bổ bộ nhớ, sau đó gọi hàm tạo mặc định và b sẽ có giá trị '0'.// Tạo đối tượng mà không cần new.A a;  // Dành không gian cho a trên ngăn xếp và b sẽ có giá trị rác không xác định.

    Tuy nhiên, nếu một hàm tạo do người dùng xác định được định nghĩa cho lớp, thì cả hai khai báo trên sẽ gọi hàm tạo do người dùng xác định này, mà mã được xác định sẽ được thực thi, nhưng không có giá trị mặc định nào được gán cho biến b.

    Hàm hủy

    Một hàm hủy là nghịch đảo của một hàm tạo. Nó được gọi khi một thể hiện của lớp bị hủy, ví dụ: khi một đối tượng của một lớp được tạo trong một khối (tập hợp các dấu ngoặc nhọn "{}") bị xóa sau dấu ngoặc nhọn đóng, thì hàm hủy được gọi tự động. Nó sẽ được gọi khi làm trống vị trí bộ nhớ lưu các biến. Bộ hủy có thể được sử dụng để giải phóng tài nguyên, chẳng hạn như bộ nhớ được cấp phát theo đống và các tệp đã mở khi một thể hiện của lớp đó bị hủy.

    Cú pháp để khai báo một hàm hủy tương tự như cú pháp của một hàm tạo. Không có giá trị trả về và tên của phương thức giống với tên của lớp có dấu ngã (~) ở phía trước.

    ~Person() {  std::cout << "I'm deleting " << name_ << " with age " << age_ << std::endl;}